/*
 * DefaultFurnitureCatalog.java 7 avr. 2006
 * 
 * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com>
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */
package com.eteks.sweethome3d.io;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessControlException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import com.eteks.sweethome3d.model.CatalogDoorOrWindow;
import com.eteks.sweethome3d.model.CatalogLight;
import com.eteks.sweethome3d.model.CatalogPieceOfFurniture;
import com.eteks.sweethome3d.model.Content;
import com.eteks.sweethome3d.model.FurnitureCatalog;
import com.eteks.sweethome3d.model.FurnitureCategory;
import com.eteks.sweethome3d.model.Library;
import com.eteks.sweethome3d.model.LightSource;
import com.eteks.sweethome3d.model.Sash;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.tools.ResourceURLContent;
import com.eteks.sweethome3d.tools.TemporaryURLContent;
import com.eteks.sweethome3d.tools.URLContent;

/**
 * Furniture default catalog read from resources localized in <code>.properties</code> files.
 * @author Emmanuel Puybaret
 */
public class DefaultFurnitureCatalog extends FurnitureCatalog
{
	/**
	 * The keys of the properties values read in <code>.properties</code> files.
	 */
	public enum PropertyKey
	{
		/**
		 * The key for the ID of a piece of furniture (optional). 
		 * Two pieces of furniture read in a furniture catalog can't have the same ID
		 * and the second one will be ignored.   
		 */
		ID("id"),
		/**
		 * The key for the name of a piece of furniture (mandatory).
		 */
		NAME("name"),
		/**
		 * The key for the description of a piece of furniture (optional). 
		 * This may give detailed information about a piece of furniture.
		 */
		DESCRIPTION("description"),
		/**
		 * The key for some additional information associated to a piece of furniture (optional).
		 * This information may contain some HTML code or a link to an external web site.
		 */
		INFORMATION("information"),
		/**
		 * The key for the tags or keywords associated to a piece of furniture (optional). 
		 * Tags are separated by commas with possible heading or trailing spaces. 
		 */
		TAGS("tags"),
		/**
		 * The key for the creation or publication date of a piece of furniture at 
		 * <code>yyyy-MM-dd</code> format (optional).
		 */
		CREATION_DATE("creationDate"),
		/**
		 * The key for the grade of a piece of furniture (optional).
		 */
		GRADE("grade"),
		/**
		 * The key for the category's name of a piece of furniture (mandatory).
		 * A new category with this name will be created if it doesn't exist.
		 */
		CATEGORY("category"),
		/**
		 * The key for the icon file of a piece of furniture (mandatory). 
		 * This icon file can be either the path to an image relative to classpath
		 * or an absolute URL. It should be encoded in application/x-www-form-urlencoded  
		 * format if needed. 
		 */
		ICON("icon"),
		/**
		 * The key for the SHA-1 digest of the icon file of a piece of furniture (optional). 
		 * This property is used to compare faster catalog resources with the ones of a read home,
		 * and should be encoded in Base64.  
		 */
		ICON_DIGEST("iconDigest"),
		/**
		 * The key for the plan icon file of a piece of furniture (optional).
		 * This icon file can be either the path to an image relative to classpath
		 * or an absolute URL. It should be encoded in application/x-www-form-urlencoded  
		 * format if needed.
		 */
		PLAN_ICON("planIcon"),
		/**
		 * The key for the SHA-1 digest of the plan icon file of a piece of furniture (optional). 
		 * This property is used to compare faster catalog resources with the ones of a read home,
		 * and should be encoded in Base64.  
		 */
		PLAN_ICON_DIGEST("planIconDigest"),
		/**
		 * The key for the 3D model file of a piece of furniture (mandatory).
		 * The 3D model file can be either a path relative to classpath
		 * or an absolute URL.  It should be encoded in application/x-www-form-urlencoded  
		 * format if needed.
		 */
		MODEL("model"),
		/**
		 * The key for the SHA-1 digest of the 3D model file of a piece of furniture (optional). 
		 * This property is used to compare faster catalog resources with the ones of a read home,
		 * and should be encoded in Base64.  
		 */
		MODEL_DIGEST("modelDigest"),
		/**
		 * The key for a piece of furniture with multiple parts (optional).
		 * If the value of this key is <code>true</code>, all the files
		 * stored in the same folder as the 3D model file (MTL, texture files...)
		 * will be considered as being necessary to view correctly the 3D model. 
		 */
		MULTI_PART_MODEL("multiPartModel"),
		/**
		 * The key for the width in centimeters of a piece of furniture (mandatory).
		 */
		WIDTH("width"),
		/**
		 * The key for the depth in centimeters of a piece of furniture (mandatory).
		 */
		DEPTH("depth"),
		/**
		 * The key for the height in centimeters of a piece of furniture (mandatory).
		 */
		HEIGHT("height"),
		/**
		 * The key for the movability of a piece of furniture (mandatory).
		 * If the value of this key is <code>true</code>, the piece of furniture
		 * will be considered as a movable piece. 
		 */
		MOVABLE("movable"),
		/**
		 * The key for the door or window type of a piece of furniture (mandatory).
		 * If the value of this key is <code>true</code>, the piece of furniture
		 * will be considered as a door or a window. 
		 */
		DOOR_OR_WINDOW("doorOrWindow"),
		/**
		 * The key for the shape of a door or window used to cut out walls when they intersect it (optional).
		 * This shape should be defined with the syntax of the d attribute of a 
		 * <a href="http://www.w3.org/TR/SVG/paths.html">SVG path element</a>
		 * and should fit in a square spreading from (0, 0) to (1, 1) which will be 
		 * scaled afterwards to the real size of the piece. 
		 * If not specified, then this shape will be automatically computed from the actual shape of the model.  
		 */
		DOOR_OR_WINDOW_CUT_OUT_SHAPE("doorOrWindowCutOutShape"),
		/**
		 * The key for the wall thickness in centimeters of a door or a window (optional).
		 * By default, a door or a window has the same depth as the wall it belongs to.
		 */
		DOOR_OR_WINDOW_WALL_THICKNESS("doorOrWindowWallThickness"),
		/**
		 * The key for the distance in centimeters of a door or a window to its wall (optional).
		 * By default, this distance is zero.
		 */
		DOOR_OR_WINDOW_WALL_DISTANCE("doorOrWindowWallDistance"),
		/**
		 * The key for the sash axis distance(s) of a door or a window along X axis (optional).
		 * If a door or a window has more than one sash, the values of each sash should be 
		 * separated by spaces.  
		 */
		DOOR_OR_WINDOW_SASH_X_AXIS("doorOrWindowSashXAxis"),
		/**
		 * The key for the sash axis distance(s) of a door or a window along Y axis 
		 * (mandatory if sash axis distance along X axis is defined).
		 */
		DOOR_OR_WINDOW_SASH_Y_AXIS("doorOrWindowSashYAxis"),
		/**
		 * The key for the sash width(s) of a door or a window  
		 * (mandatory if sash axis distance along X axis is defined).
		 */
		DOOR_OR_WINDOW_SASH_WIDTH("doorOrWindowSashWidth"),
		/**
		 * The key for the sash start angle(s) of a door or a window  
		 * (mandatory if sash axis distance along X axis is defined).
		 */
		DOOR_OR_WINDOW_SASH_START_ANGLE("doorOrWindowSashStartAngle"),
		/**
		 * The key for the sash end angle(s) of a door or a window  
		 * (mandatory if sash axis distance along X axis is defined).
		 */
		DOOR_OR_WINDOW_SASH_END_ANGLE("doorOrWindowSashEndAngle"),
		/**
		 * The key for the abscissa(s) of light sources in a light (optional).
		 * If a light has more than one light source, the values of each light source should 
		 * be separated by spaces.
		 */
		LIGHT_SOURCE_X("lightSourceX"),
		/**
		 * The key for the ordinate(s) of light sources in a light (mandatory if light source abscissa is defined).
		 */
		LIGHT_SOURCE_Y("lightSourceY"),
		/**
		 * The key for the elevation(s) of light sources in a light (mandatory if light source abscissa is defined).
		 */
		LIGHT_SOURCE_Z("lightSourceZ"),
		/**
		 * The key for the color(s) of light sources in a light (mandatory if light source abscissa is defined).
		 */
		LIGHT_SOURCE_COLOR("lightSourceColor"),
		/**
		 * The key for the color(s) of light sources in a light (optional).
		 */
		LIGHT_SOURCE_DIAMETER("lightSourceDiameter"),
		/**
		 * The key for the shape used to cut out upper levels when they intersect with a piece   
		 * like a staircase (optional). This shape should be defined with the syntax of 
		 * the d attribute of a <a href="http://www.w3.org/TR/SVG/paths.html">SVG path element</a>
		 * and should fit in a square spreading from (0, 0) to (1, 1) which will be scaled afterwards 
		 * to the real size of the piece. 
		 */
		STAIRCASE_CUT_OUT_SHAPE("staircaseCutOutShape"),
		/**
		 * The key for the elevation in centimeters of a piece of furniture (optional).
		 */
		ELEVATION("elevation"),
		/**
		 * The key for the preferred elevation (from the bottom of a piece) at which should be placed  
		 * an object dropped on a piece (optional). A negative value means that the piece should be ignored
		 * when an object is dropped on it. By default, this elevation is equal to its height. 
		 */
		DROP_ON_TOP_ELEVATION("dropOnTopElevation"),
		/**
		 * The key for the transformation matrix values applied to a piece of furniture (optional).
		 * If the 3D model of a piece of furniture isn't correctly oriented, 
		 * the value of this key should give the 9 values of the transformation matrix 
		 * that will orient it correctly.  
		 */
		MODEL_ROTATION("modelRotation"),
		/**
		 * The key for the creator of a piece of furniture (optional).
		 * By default, creator is eTeks.
		 */
		CREATOR("creator"),
		/**
		 * The key for the resizability of a piece of furniture (optional, <code>true</code> by default).
		 * If the value of this key is <code>false</code>, the piece of furniture
		 * will be considered as a piece with a fixed size. 
		 */
		RESIZABLE("resizable"),
		/**
		 * The key for the deformability of a piece of furniture (optional, <code>true</code> by default).
		 * If the value of this key is <code>false</code>, the piece of furniture
		 * will be considered as a piece that should always keep its proportions when resized. 
		 */
		DEFORMABLE("deformable"),
		/**
		 * The key for the texturable capability of a piece of furniture (optional, <code>true</code> by default).
		 * If the value of this key is <code>false</code>, the piece of furniture
		 * will be considered as a piece that will always keep the same color or texture. 
		 */
		TEXTURABLE("texturable"),
		/**
		 * The key for the price of a piece of furniture (optional).
		 */
		PRICE("price"),
		/**
		 * The key for the VAT percentage of a piece of furniture (optional).
		 */
		VALUE_ADDED_TAX_PERCENTAGE("valueAddedTaxPercentage"),
		/**
		 * The key for the currency ISO 4217 code of the price of a piece of furniture (optional).
		 */
		CURRENCY("currency");
		
		private String keyPrefix;
		
		private PropertyKey(String keyPrefix)
		{
			this.keyPrefix = keyPrefix;
		}
		
		/**
		 * Returns the key for the piece property of the given index.
		 */
		public String getKey(int pieceIndex)
		{
			return keyPrefix + "#" + pieceIndex;
		}
	}
	
	/**
	 * The name of <code>.properties</code> family files in plugin furniture catalog files. 
	 */
	public static final String PLUGIN_FURNITURE_CATALOG_FAMILY = "PluginFurnitureCatalog";
	
	private static final String CONTRIBUTED_FURNITURE_CATALOG_FAMILY = "ContributedFurnitureCatalog";
	private static final String ADDITIONAL_FURNITURE_CATALOG_FAMILY = "AdditionalFurnitureCatalog";
	
	private List<Library> libraries = new ArrayList<Library>();
	
	/**
	 * Creates a default furniture catalog read from resources in the package of this class.
	 */
	public DefaultFurnitureCatalog()
	{
		this((File) null);
	}
	
	/**
	 * Creates a default furniture catalog read from resources and   
	 * furniture plugin folder if <code>furniturePluginFolder</code> isn't <code>null</code>.
	 */
	public DefaultFurnitureCatalog(File furniturePluginFolder)
	{
		this(null, furniturePluginFolder);
	}
	
	/**
	 * Creates a default furniture catalog read from resources and   
	 * furniture plugin folder if <code>furniturePluginFolder</code> isn't <code>null</code>.
	 */
	public DefaultFurnitureCatalog(final UserPreferences preferences, File furniturePluginFolder)
	{
		this(preferences, furniturePluginFolder == null ? null : new File[] { furniturePluginFolder });
	}
	
	/**
	 * Creates a default furniture catalog read from resources and   
	 * furniture plugin folders if <code>furniturePluginFolders</code> isn't <code>null</code>.
	 */
	public DefaultFurnitureCatalog(final UserPreferences preferences, File[] furniturePluginFolders)
	{
		Map<FurnitureCategory, Map<CatalogPieceOfFurniture, Integer>> furnitureHomonymsCounter = new HashMap<FurnitureCategory, Map<CatalogPieceOfFurniture, Integer>>();
		List<String> identifiedFurniture = new ArrayList<String>();
		
		readDefaultFurnitureCatalogs(preferences, furnitureHomonymsCounter, identifiedFurniture);
		
		if (furniturePluginFolders != null)
		{
			for (File furniturePluginFolder : furniturePluginFolders)
			{
				// Try to load sh3f files from furniture plugin folder
				File[] pluginFurnitureCatalogFiles = furniturePluginFolder.listFiles(new FileFilter()
				{
					public boolean accept(File pathname)
					{
						return pathname.isFile();
					}
				});
				
				if (pluginFurnitureCatalogFiles != null)
				{
					// Treat furniture catalog files in reverse order of their version
					Arrays.sort(pluginFurnitureCatalogFiles,
							Collections.reverseOrder(OperatingSystem.getFileVersionComparator()));
					for (File pluginFurnitureCatalogFile : pluginFurnitureCatalogFiles)
					{
						// Try to load the properties file describing furniture catalog from current file  
						readPluginFurnitureCatalog(pluginFurnitureCatalogFile, identifiedFurniture);
					}
				}
			}
		}
	}
	
	/**
	 * Creates a default furniture catalog read only from resources in the given URLs.
	 */
	public DefaultFurnitureCatalog(URL[] pluginFurnitureCatalogUrls)
	{
		this(pluginFurnitureCatalogUrls, null);
	}
	
	/**
	 * Creates a default furniture catalog read only from resources in the given URLs 
	 * or in the classpath if the security manager doesn't allow to create class loaders.
	 * Model and icon URLs will built from <code>furnitureResourcesUrlBase</code> if it isn't <code>null</code>.
	 */
	public DefaultFurnitureCatalog(URL[] pluginFurnitureCatalogUrls, URL furnitureResourcesUrlBase)
	{
		List<String> identifiedFurniture = new ArrayList<String>();
		try
		{
			SecurityManager securityManager = System.getSecurityManager();
			if (securityManager != null)
			{
				securityManager.checkCreateClassLoader();
			}
			
			for (URL pluginFurnitureCatalogUrl : pluginFurnitureCatalogUrls)
			{
				try
				{
					ResourceBundle resource = ResourceBundle.getBundle(PLUGIN_FURNITURE_CATALOG_FAMILY,
							Locale.getDefault(), new URLContentClassLoader(pluginFurnitureCatalogUrl));
					this.libraries.add(0, new DefaultLibrary(pluginFurnitureCatalogUrl.toExternalForm(),
							UserPreferences.FURNITURE_LIBRARY_TYPE, resource));
					readFurniture(resource, pluginFurnitureCatalogUrl, furnitureResourcesUrlBase, identifiedFurniture);
				}
				catch (MissingResourceException ex)
				{
					// Ignore malformed furniture catalog
				}
				catch (IllegalArgumentException ex)
				{
					// Ignore malformed furniture catalog
				}
			}
		}
		catch (AccessControlException ex)
		{
			// Use only furniture accessible through classpath
			ResourceBundle resource = ResourceBundle.getBundle(PLUGIN_FURNITURE_CATALOG_FAMILY, Locale.getDefault());
			readFurniture(resource, null, furnitureResourcesUrlBase, identifiedFurniture);
		}
	}
	
	/**
	 * Returns the furniture libraries at initialization.
	 * @since 4.0 
	 */
	public List<Library> getLibraries()
	{
		return Collections.unmodifiableList(this.libraries);
	}
	
	private static final Map<File, URL> pluginFurnitureCatalogUrlUpdates = new HashMap<File, URL>();
	
	/**
	 * Reads plug-in furniture catalog from the <code>pluginFurnitureCatalogFile</code> file. 
	 */
	private void readPluginFurnitureCatalog(File pluginFurnitureCatalogFile, List<String> identifiedFurniture)
	{
		try
		{
			final URL pluginFurnitureCatalogUrl;
			long urlModificationDate = pluginFurnitureCatalogFile.lastModified();
			URL urlUpdate = pluginFurnitureCatalogUrlUpdates.get(pluginFurnitureCatalogFile);
			if (pluginFurnitureCatalogFile.canWrite()
					&& (urlUpdate == null || urlUpdate.openConnection().getLastModified() < urlModificationDate))
			{
				// Copy updated resource URL content to a temporary file to ensure furniture added to home can safely 
				// reference any file of the catalog file even if its content is changed afterwards
				TemporaryURLContent contentCopy = TemporaryURLContent
						.copyToTemporaryURLContent(new URLContent(pluginFurnitureCatalogFile.toURI().toURL()));
				URL temporaryFurnitureCatalogUrl = contentCopy.getURL();
				pluginFurnitureCatalogUrlUpdates.put(pluginFurnitureCatalogFile, temporaryFurnitureCatalogUrl);
				pluginFurnitureCatalogUrl = temporaryFurnitureCatalogUrl;
			}
			else if (urlUpdate != null)
			{
				pluginFurnitureCatalogUrl = urlUpdate;
			}
			else
			{
				pluginFurnitureCatalogUrl = pluginFurnitureCatalogFile.toURI().toURL();
			}
			
			final ClassLoader urlLoader = new URLContentClassLoader(pluginFurnitureCatalogUrl);
			ResourceBundle resourceBundle = ResourceBundle.getBundle(PLUGIN_FURNITURE_CATALOG_FAMILY,
					Locale.getDefault(), urlLoader);
			this.libraries.add(0, new DefaultLibrary(pluginFurnitureCatalogFile.getCanonicalPath(),
					UserPreferences.FURNITURE_LIBRARY_TYPE, resourceBundle));
			readFurniture(resourceBundle, pluginFurnitureCatalogUrl, null, identifiedFurniture);
		}
		catch (MissingResourceException ex)
		{
			// Ignore malformed furniture catalog
		}
		catch (IllegalArgumentException ex)
		{
			// Ignore malformed furniture catalog
		}
		catch (IOException ex)
		{
			// Ignore unaccessible catalog
		}
	}
	
	/**
	 * Reads the default furniture described in properties files accessible through classpath.
	 */
	private void readDefaultFurnitureCatalogs(UserPreferences preferences,
			Map<FurnitureCategory, Map<CatalogPieceOfFurniture, Integer>> furnitureHomonymsCounter,
			List<String> identifiedFurniture)
	{
		// Try to load com.eteks.sweethome3d.io.DefaultFurnitureCatalog property file from classpath 
		String defaultFurnitureCatalogFamily = DefaultFurnitureCatalog.class.getName();
		readFurnitureCatalog(defaultFurnitureCatalogFamily, preferences, furnitureHomonymsCounter, identifiedFurniture);
		
		// Try to load com.eteks.sweethome3d.io.ContributedFurnitureCatalog property file from classpath 
		String classPackage = defaultFurnitureCatalogFamily.substring(0,
				defaultFurnitureCatalogFamily.lastIndexOf("."));
		readFurnitureCatalog(classPackage + "." + CONTRIBUTED_FURNITURE_CATALOG_FAMILY, preferences,
				furnitureHomonymsCounter, identifiedFurniture);
		
		// Try to load com.eteks.sweethome3d.io.AdditionalFurnitureCatalog property file from classpath
		readFurnitureCatalog(classPackage + "." + ADDITIONAL_FURNITURE_CATALOG_FAMILY, preferences,
				furnitureHomonymsCounter, identifiedFurniture);
	}
	
	/**
	 * Reads furniture of a given catalog family from resources.
	 */
	private void readFurnitureCatalog(final String furnitureCatalogFamily, final UserPreferences preferences,
			Map<FurnitureCategory, Map<CatalogPieceOfFurniture, Integer>> furnitureHomonymsCounter,
			List<String> identifiedFurniture)
	{
		ResourceBundle resource;
		if (preferences != null)
		{
			// Adapt getLocalizedString to ResourceBundle
			resource = new ResourceBundle()
			{
				@Override
				protected Object handleGetObject(String key)
				{
					try
					{
						return preferences.getLocalizedString(furnitureCatalogFamily, key);
					}
					catch (IllegalArgumentException ex)
					{
						throw new MissingResourceException("Unknown key " + key,
								furnitureCatalogFamily + "_" + Locale.getDefault(), key);
					}
				}
				
				@Override
				public Enumeration<String> getKeys()
				{
					// Not needed
					throw new UnsupportedOperationException();
				}
			};
		}
		else
		{
			try
			{
				resource = ResourceBundle.getBundle(furnitureCatalogFamily);
			}
			catch (MissingResourceException ex)
			{
				return;
			}
		}
		readFurniture(resource, null, null, identifiedFurniture);
	}
	
	/**
	 * Reads each piece of furniture described in <code>resource</code> bundle.
	 * Resources described in piece properties will be loaded from <code>furnitureCatalogUrl</code> 
	 * if it isn't <code>null</code> or relative to <code>furnitureResourcesUrlBase</code>. 
	 */
	private void readFurniture(ResourceBundle resource, URL furnitureCatalogUrl, URL furnitureResourcesUrlBase,
			List<String> identifiedFurniture)
	{
		CatalogPieceOfFurniture piece;
		for (int i = 1; (piece = readPieceOfFurniture(resource, i, furnitureCatalogUrl,
				furnitureResourcesUrlBase)) != null; i++)
		{
			if (piece.getId() != null)
			{
				// Take into account only furniture that have an ID
				if (identifiedFurniture.contains(piece.getId()))
				{
					continue;
				}
				else
				{
					// Add id to identifiedFurniture to be sure that two pieces with a same ID
					// won't be added twice to furniture catalog (in case they are cited twice
					// in different furniture properties files)
					identifiedFurniture.add(piece.getId());
				}
			}
			FurnitureCategory pieceCategory = readFurnitureCategory(resource, i);
			add(pieceCategory, piece);
		}
	}
	
	/**
	 * Returns the piece of furniture at the given <code>index</code> of a 
	 * localized <code>resource</code> bundle. 
	 * @param resource             a resource bundle 
	 * @param index                the index of the read piece
	 * @param furnitureCatalogUrl  the URL from which piece resources will be loaded 
	 *            or <code>null</code> if it's read from current classpath.
	 * @param furnitureResourcesUrlBase the URL used as a base to build the URL to piece resources  
	 *            or <code>null</code> if it's read from current classpath or <code>furnitureCatalogUrl</code>
	 * @return the read piece of furniture or <code>null</code> if the piece at the given index doesn't exist.
	 * @throws MissingResourceException if mandatory keys are not defined.
	 */
	protected CatalogPieceOfFurniture readPieceOfFurniture(ResourceBundle resource, int index, URL furnitureCatalogUrl,
			URL furnitureResourcesUrlBase)
	{
		String name = null;
		try
		{
			name = resource.getString(PropertyKey.NAME.getKey(index));
		}
		catch (MissingResourceException ex)
		{
			// Return null if key name# doesn't exist
			return null;
		}
		String id = getOptionalString(resource, PropertyKey.ID.getKey(index), null);
		String description = getOptionalString(resource, PropertyKey.DESCRIPTION.getKey(index), null);
		String information = getOptionalString(resource, PropertyKey.INFORMATION.getKey(index), null);
		String tagsString = getOptionalString(resource, PropertyKey.TAGS.getKey(index), null);
		String[] tags;
		if (tagsString != null)
		{
			tags = tagsString.split("\\s*,\\s*");
		}
		else
		{
			tags = new String[0];
		}
		String creationDateString = getOptionalString(resource, PropertyKey.CREATION_DATE.getKey(index), null);
		Long creationDate = null;
		if (creationDateString != null)
		{
			try
			{
				creationDate = new SimpleDateFormat("yyyy-MM-dd").parse(creationDateString).getTime();
			}
			catch (ParseException ex)
			{
				throw new IllegalArgumentException("Can't parse date " + creationDateString, ex);
			}
		}
		String gradeString = getOptionalString(resource, PropertyKey.GRADE.getKey(index), null);
		Float grade = null;
		if (gradeString != null)
		{
			grade = Float.valueOf(gradeString);
		}
		Content icon = getContent(resource, PropertyKey.ICON.getKey(index), PropertyKey.ICON_DIGEST.getKey(index),
				furnitureCatalogUrl, furnitureResourcesUrlBase, false, false);
		Content planIcon = getContent(resource, PropertyKey.PLAN_ICON.getKey(index),
				PropertyKey.PLAN_ICON_DIGEST.getKey(index), furnitureCatalogUrl, furnitureResourcesUrlBase, false,
				true);
		boolean multiPartModel = getOptionalBoolean(resource, PropertyKey.MULTI_PART_MODEL.getKey(index), false);
		Content model = getContent(resource, PropertyKey.MODEL.getKey(index), PropertyKey.MODEL_DIGEST.getKey(index),
				furnitureCatalogUrl, furnitureResourcesUrlBase, multiPartModel, false);
		float width = Float.parseFloat(resource.getString(PropertyKey.WIDTH.getKey(index)));
		float depth = Float.parseFloat(resource.getString(PropertyKey.DEPTH.getKey(index)));
		float height = Float.parseFloat(resource.getString(PropertyKey.HEIGHT.getKey(index)));
		float elevation = getOptionalFloat(resource, PropertyKey.ELEVATION.getKey(index), 0);
		float dropOnTopElevation = getOptionalFloat(resource, PropertyKey.DROP_ON_TOP_ELEVATION.getKey(index), height)
				/ height;
		boolean movable = Boolean.parseBoolean(resource.getString(PropertyKey.MOVABLE.getKey(index)));
		boolean doorOrWindow = Boolean.parseBoolean(resource.getString(PropertyKey.DOOR_OR_WINDOW.getKey(index)));
		String staircaseCutOutShape = getOptionalString(resource, PropertyKey.STAIRCASE_CUT_OUT_SHAPE.getKey(index),
				null);
		float[][] modelRotation = getModelRotation(resource, PropertyKey.MODEL_ROTATION.getKey(index));
		// By default creator is eTeks
		String creator = getOptionalString(resource, PropertyKey.CREATOR.getKey(index), null);
		boolean resizable = getOptionalBoolean(resource, PropertyKey.RESIZABLE.getKey(index), true);
		boolean deformable = getOptionalBoolean(resource, PropertyKey.DEFORMABLE.getKey(index), true);
		boolean texturable = getOptionalBoolean(resource, PropertyKey.TEXTURABLE.getKey(index), true);
		BigDecimal price = null;
		try
		{
			price = new BigDecimal(resource.getString(PropertyKey.PRICE.getKey(index)));
		}
		catch (MissingResourceException ex)
		{
			// By default price is null
		}
		BigDecimal valueAddedTaxPercentage = null;
		try
		{
			valueAddedTaxPercentage = new BigDecimal(
					resource.getString(PropertyKey.VALUE_ADDED_TAX_PERCENTAGE.getKey(index)));
		}
		catch (MissingResourceException ex)
		{
			// By default price is null
		}
		String currency = getOptionalString(resource, PropertyKey.CURRENCY.getKey(index), null);
		
		if (doorOrWindow)
		{
			String doorOrWindowCutOutShape = getOptionalString(resource,
					PropertyKey.DOOR_OR_WINDOW_CUT_OUT_SHAPE.getKey(index), null);
			float wallThicknessPercentage = getOptionalFloat(resource,
					PropertyKey.DOOR_OR_WINDOW_WALL_THICKNESS.getKey(index), depth) / depth;
			float wallDistancePercentage = getOptionalFloat(resource,
					PropertyKey.DOOR_OR_WINDOW_WALL_DISTANCE.getKey(index), 0) / depth;
			Sash[] sashes = getDoorOrWindowSashes(resource, index, width, depth);
			return new CatalogDoorOrWindow(id, name, description, information, tags, creationDate, grade, icon,
					planIcon, model, width, depth, height, elevation, dropOnTopElevation, movable,
					doorOrWindowCutOutShape, wallThicknessPercentage, wallDistancePercentage, sashes, modelRotation,
					creator, resizable, deformable, texturable, price, valueAddedTaxPercentage, currency);
		}
		else
		{
			LightSource[] lightSources = getLightSources(resource, index, width, depth, height);
			if (lightSources != null)
			{
				return new CatalogLight(id, name, description, information, tags, creationDate, grade, icon, planIcon,
						model, width, depth, height, elevation, dropOnTopElevation, movable, lightSources,
						staircaseCutOutShape, modelRotation, creator, resizable, deformable, texturable, price,
						valueAddedTaxPercentage, currency);
			}
			else
			{
				return new CatalogPieceOfFurniture(id, name, description, information, tags, creationDate, grade, icon,
						planIcon, model, width, depth, height, elevation, dropOnTopElevation, movable,
						staircaseCutOutShape, modelRotation, creator, resizable, deformable, texturable, price,
						valueAddedTaxPercentage, currency);
			}
		}
	}
	
	/**
	 * Returns the furniture category of a piece at the given <code>index</code> of a 
	 * localized <code>resource</code> bundle. 
	 * @throws MissingResourceException if mandatory keys are not defined.
	 */
	protected FurnitureCategory readFurnitureCategory(ResourceBundle resource, int index)
	{
		String category = resource.getString(PropertyKey.CATEGORY.getKey(index));
		return new FurnitureCategory(category);
	}
	
	/**
	 * Returns a valid content instance from the resource file or URL value of key.
	 * @param resource a resource bundle
	 * @param contentKey        the key of a resource content file
	 * @param contentDigestKey  the key of the digest of a resource content file
	 * @param furnitureUrl the URL of the file containing the target resource if it's not <code>null</code> 
	 * @param resourceUrlBase the URL used as a base to build the URL to content file  
	 *            or <code>null</code> if it's read from current classpath or <code>furnitureCatalogUrl</code>.
	 * @param multiPartModel if <code>true</code> the resource is a multi part resource stored 
	 *                 in a folder with other required resources
	 * @throws IllegalArgumentException if the file value doesn't match a valid resource or URL.
	 */
	private Content getContent(ResourceBundle resource, String contentKey, String contentDigestKey, URL furnitureUrl,
			URL resourceUrlBase, boolean multiPartModel, boolean optional)
	{
		String contentFile = optional ? getOptionalString(resource, contentKey, null) : resource.getString(contentKey);
		if (optional && contentFile == null)
		{
			return null;
		}
		URLContent content;
		try
		{
			// Try first to interpret contentFile as an absolute URL 
			// or an URL relative to resourceUrlBase if it's not null
			URL url;
			if (resourceUrlBase == null)
			{
				url = new URL(contentFile);
			}
			else
			{
				url = contentFile.startsWith("?") ? new URL(resourceUrlBase + contentFile)
						: new URL(resourceUrlBase, contentFile);
				if (contentFile.indexOf('!') >= 0 && !contentFile.startsWith("jar:"))
				{
					url = new URL("jar:" + url);
				}
			}
			content = new URLContent(url);
		}
		catch (MalformedURLException ex)
		{
			if (furnitureUrl == null)
			{
				// Otherwise find if it's a resource
				content = new ResourceURLContent(DefaultFurnitureCatalog.class, contentFile, multiPartModel);
			}
			else
			{
				try
				{
					content = new ResourceURLContent(new URL("jar:" + furnitureUrl + "!" + contentFile),
							multiPartModel);
				}
				catch (MalformedURLException ex2)
				{
					throw new IllegalArgumentException("Invalid URL", ex2);
				}
			}
		}
		
		// Store content digest if it exists
		// Except in special cases like URL content in applets where it might avoid to download content  
		// to compute its digest, it's not recommended to store digests in sh3f and imported files. 
		// Missing digests will be computed on demand, ensuring it will be updated in case content is damaged
		String contentDigest = getOptionalString(resource, contentDigestKey, null);
		if (contentDigest != null && contentDigest.length() > 0)
		{
			try
			{
				ContentDigestManager.getInstance().setContentDigest(content, Base64.decode(contentDigest));
			}
			catch (IOException ex)
			{
				// Ignore wrong digest
			}
		}
		return content;
	}
	
	/**
	 * Returns model rotation parsed from key value.
	 */
	private float[][] getModelRotation(ResourceBundle resource, String key)
	{
		try
		{
			String modelRotationString = resource.getString(key);
			String[] values = modelRotationString.split(" ", 9);
			
			if (values.length == 9)
			{
				return new float[][] {
						{ Float.parseFloat(values[0]), Float.parseFloat(values[1]), Float.parseFloat(values[2]) },
						{ Float.parseFloat(values[3]), Float.parseFloat(values[4]), Float.parseFloat(values[5]) },
						{ Float.parseFloat(values[6]), Float.parseFloat(values[7]), Float.parseFloat(values[8]) } };
			}
			else
			{
				return null;
			}
		}
		catch (MissingResourceException ex)
		{
			return null;
		}
		catch (NumberFormatException ex)
		{
			return null;
		}
	}
	
	/**
	 * Returns optional door or windows sashes.
	 */
	private Sash[] getDoorOrWindowSashes(ResourceBundle resource, int index, float doorOrWindowWidth,
			float doorOrWindowDepth) throws MissingResourceException
	{
		Sash[] sashes;
		String sashXAxisString = getOptionalString(resource, PropertyKey.DOOR_OR_WINDOW_SASH_X_AXIS.getKey(index),
				null);
		if (sashXAxisString != null)
		{
			String[] sashXAxisValues = sashXAxisString.split(" ");
			// If doorOrWindowHingesX#i key exists the 3 other keys with the same count of numbers must exist too
			String[] sashYAxisValues = resource.getString(PropertyKey.DOOR_OR_WINDOW_SASH_Y_AXIS.getKey(index))
					.split(" ");
			if (sashYAxisValues.length != sashXAxisValues.length)
			{
				throw new IllegalArgumentException("Expected " + sashXAxisValues.length + " values in "
						+ PropertyKey.DOOR_OR_WINDOW_SASH_Y_AXIS.getKey(index) + " key");
			}
			String[] sashWidths = resource.getString(PropertyKey.DOOR_OR_WINDOW_SASH_WIDTH.getKey(index)).split(" ");
			if (sashWidths.length != sashXAxisValues.length)
			{
				throw new IllegalArgumentException("Expected " + sashXAxisValues.length + " values in "
						+ PropertyKey.DOOR_OR_WINDOW_SASH_WIDTH.getKey(index) + " key");
			}
			String[] sashStartAngles = resource.getString(PropertyKey.DOOR_OR_WINDOW_SASH_START_ANGLE.getKey(index))
					.split(" ");
			if (sashStartAngles.length != sashXAxisValues.length)
			{
				throw new IllegalArgumentException("Expected " + sashXAxisValues.length + " values in "
						+ PropertyKey.DOOR_OR_WINDOW_SASH_START_ANGLE.getKey(index) + " key");
			}
			String[] sashEndAngles = resource.getString(PropertyKey.DOOR_OR_WINDOW_SASH_END_ANGLE.getKey(index))
					.split(" ");
			if (sashEndAngles.length != sashXAxisValues.length)
			{
				throw new IllegalArgumentException("Expected " + sashXAxisValues.length + " values in "
						+ PropertyKey.DOOR_OR_WINDOW_SASH_END_ANGLE.getKey(index) + " key");
			}
			
			sashes = new Sash[sashXAxisValues.length];
			for (int i = 0; i < sashes.length; i++)
			{
				// Create the matching sash, converting cm to percentage of width or depth, and degrees to radians
				sashes[i] = new Sash(Float.parseFloat(sashXAxisValues[i]) / doorOrWindowWidth,
						Float.parseFloat(sashYAxisValues[i]) / doorOrWindowDepth,
						Float.parseFloat(sashWidths[i]) / doorOrWindowWidth,
						(float) Math.toRadians(Float.parseFloat(sashStartAngles[i])),
						(float) Math.toRadians(Float.parseFloat(sashEndAngles[i])));
			}
		}
		else
		{
			sashes = new Sash[0];
		}
		
		return sashes;
	}
	
	/**
	 * Returns optional light sources.
	 */
	private LightSource[] getLightSources(ResourceBundle resource, int index, float lightWidth, float lightDepth,
			float lightHeight) throws MissingResourceException
	{
		LightSource[] lightSources = null;
		String lightSourceXString = getOptionalString(resource, PropertyKey.LIGHT_SOURCE_X.getKey(index), null);
		if (lightSourceXString != null)
		{
			String[] lightSourceX = lightSourceXString.split(" ");
			// If doorOrWindowHingesX#i key exists the 3 other keys with the same count of numbers must exist too
			String[] lightSourceY = resource.getString(PropertyKey.LIGHT_SOURCE_Y.getKey(index)).split(" ");
			if (lightSourceY.length != lightSourceX.length)
			{
				throw new IllegalArgumentException("Expected " + lightSourceX.length + " values in "
						+ PropertyKey.LIGHT_SOURCE_Y.getKey(index) + " key");
			}
			String[] lightSourceZ = resource.getString(PropertyKey.LIGHT_SOURCE_Z.getKey(index)).split(" ");
			if (lightSourceZ.length != lightSourceX.length)
			{
				throw new IllegalArgumentException("Expected " + lightSourceX.length + " values in "
						+ PropertyKey.LIGHT_SOURCE_Z.getKey(index) + " key");
			}
			String[] lightSourceColors = resource.getString(PropertyKey.LIGHT_SOURCE_COLOR.getKey(index)).split(" ");
			if (lightSourceColors.length != lightSourceX.length)
			{
				throw new IllegalArgumentException("Expected " + lightSourceX.length + " values in "
						+ PropertyKey.LIGHT_SOURCE_COLOR.getKey(index) + " key");
			}
			String lightSourceDiametersString = getOptionalString(resource,
					PropertyKey.LIGHT_SOURCE_DIAMETER.getKey(index), null);
			String[] lightSourceDiameters;
			if (lightSourceDiametersString != null)
			{
				lightSourceDiameters = lightSourceDiametersString.split(" ");
				if (lightSourceDiameters.length != lightSourceX.length)
				{
					throw new IllegalArgumentException("Expected " + lightSourceX.length + " values in "
							+ PropertyKey.LIGHT_SOURCE_DIAMETER.getKey(index) + " key");
				}
			}
			else
			{
				lightSourceDiameters = null;
			}
			
			lightSources = new LightSource[lightSourceX.length];
			for (int i = 0; i < lightSources.length; i++)
			{
				int color = lightSourceColors[i].startsWith("#")
						? Integer.parseInt(lightSourceColors[i].substring(1), 16)
						: Integer.parseInt(lightSourceColors[i]);
				// Create the matching light source, converting cm to percentage of width, depth and height
				lightSources[i] = new LightSource(Float.parseFloat(lightSourceX[i]) / lightWidth,
						Float.parseFloat(lightSourceY[i]) / lightDepth, Float.parseFloat(lightSourceZ[i]) / lightHeight,
						color,
						lightSourceDiameters != null ? Float.parseFloat(lightSourceDiameters[i]) / lightWidth : null);
			}
		}
		return lightSources;
	}
	
	/**
	 * Returns the value of <code>propertyKey</code> in <code>resource</code>, 
	 * or <code>defaultValue</code> if the property doesn't exist.
	 */
	private String getOptionalString(ResourceBundle resource, String propertyKey, String defaultValue)
	{
		try
		{
			return resource.getString(propertyKey);
		}
		catch (MissingResourceException ex)
		{
			return defaultValue;
		}
	}
	
	/**
	 * Returns the value of <code>propertyKey</code> in <code>resource</code>, 
	 * or <code>defaultValue</code> if the property doesn't exist.
	 */
	private float getOptionalFloat(ResourceBundle resource, String propertyKey, float defaultValue)
	{
		try
		{
			return Float.parseFloat(resource.getString(propertyKey));
		}
		catch (MissingResourceException ex)
		{
			return defaultValue;
		}
	}
	
	/**
	 * Returns the boolean value of <code>propertyKey</code> in <code>resource</code>, 
	 * or <code>defaultValue</code> if the property doesn't exist.
	 */
	private boolean getOptionalBoolean(ResourceBundle resource, String propertyKey, boolean defaultValue)
	{
		try
		{
			return Boolean.parseBoolean(resource.getString(propertyKey));
		}
		catch (MissingResourceException ex)
		{
			return defaultValue;
		}
	}
}
